<!DOCTYPE html>
<!--
Copyright 2016 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<!--
  Include all Diagnostic subclasses here so that DiagnosticMap.addDicts() and
  DiagnosticMap.fromDict() always have access to all subclasses in the
  Diagnostic registry.
-->
<link rel="import" href="/tracing/value/diagnostics/all_diagnostics.html">
<link rel="import" href="/tracing/value/diagnostics/reserved_names.html">

<script>
'use strict';

tr.exportTo('tr.v.d', function() {
  class DiagnosticMap extends Map {
    /**
     * @param {boolean=} opt_allowReservedNames defaults to true
     */
    constructor(opt_allowReservedNames) {
      super();
      if (opt_allowReservedNames === undefined) {
        opt_allowReservedNames = true;
      }
      this.allowReservedNames_ = opt_allowReservedNames;
    }

    /**
     * Add a new Diagnostic to this map.
     *
     * @param {string} name
     * @param {!tr.v.d.Diagnostic} diagnostic
     */
    set(name, diagnostic) {
      if (typeof(name) !== 'string') {
        throw new Error(`name must be string, not ${name}`);
      }

      if (!(diagnostic instanceof tr.v.d.Diagnostic) &&
          !(diagnostic instanceof tr.v.d.DiagnosticRef)) {
        throw new Error(`Must be instanceof Diagnostic: ${diagnostic}`);
      }

      // TODO(#3507): Reserved names should never be UnmergeableDiagnosticSet.
      if (!this.allowReservedNames_ &&
          tr.v.d.RESERVED_NAMES_SET.has(name) &&
          !(diagnostic instanceof tr.v.d.UnmergeableDiagnosticSet) &&
          !(diagnostic instanceof tr.v.d.DiagnosticRef)) {
        const type = tr.v.d.RESERVED_NAMES_TO_TYPES.get(name);
        if (type && !(diagnostic instanceof type)) {
          throw new Error(
              `Diagnostics named "${name}" must be ${type.name}, ` +
              `not ${diagnostic.constructor.name}`);
        }
      }

      Map.prototype.set.call(this, name, diagnostic);
    }

    delete(name) {
      if (name === undefined) throw new Error('missing name');
      Map.prototype.delete.call(this, name);
    }

    deserializeAdd(data, deserializer) {
      for (const id of data) {
        const {name, diagnostic} = deserializer.getDiagnostic(id);
        this.set(name, diagnostic);
      }
    }

    /**
     * Add Diagnostics from a dictionary of dictionaries.
     *
     * @param {Object} dict
     */
    addDicts(dict) {
      for (const [name, diagnosticDict] of Object.entries(dict)) {
        if (name === 'tagmap') continue;
        if (typeof diagnosticDict === 'string') {
          this.set(name, new tr.v.d.DiagnosticRef(diagnosticDict));
        } else if (diagnosticDict.type !== 'RelatedHistogramMap' &&
                   diagnosticDict.type !== 'RelatedHistogramBreakdown' &&
                   diagnosticDict.type !== 'TagMap') {
          // Ignore RelatedHistograms and TagMaps.
          // TODO(benjhayden): Forget about them in 2019 Q2.
          this.set(name, tr.v.d.Diagnostic.fromDict(diagnosticDict));
        }
      }
    }

    resolveSharedDiagnostics(histograms, opt_required) {
      for (const [name, value] of this) {
        if (!(value instanceof tr.v.d.DiagnosticRef)) {
          continue;
        }

        const guid = value.guid;
        const diagnostic = histograms.lookupDiagnostic(guid);
        if (diagnostic instanceof tr.v.d.Diagnostic) {
          this.set(name, diagnostic);
        } else if (opt_required) {
          throw new Error('Unable to find shared Diagnostic ' + guid);
        }
      }
    }

    serialize(serializer) {
      const data = [];
      for (const [name, diagnostic] of this) {
        data.push(serializer.getOrAllocateDiagnosticId(name, diagnostic));
      }
      return data;
    }

    asDict() {
      const dict = {};
      for (const [name, diagnostic] of this) {
        dict[name] = diagnostic.asDictOrReference();
      }
      return dict;
    }

    static deserialize(data, deserializer) {
      const diagnostics = new DiagnosticMap();
      diagnostics.deserializeAdd(data, deserializer);
      return diagnostics;
    }

    static fromDict(d) {
      const diagnostics = new DiagnosticMap();
      diagnostics.addDicts(d);
      return diagnostics;
    }

    /**
     * Convert dictionary or ES6 Map to DiagnosticMap.
     * @param {!Object|!Map.<string, !tr.v.d.Diagnostic>} obj
     * @return {tr.v.d.DiagnosticMap}
     */
    static fromObject(obj) {
      const diagnostics = new DiagnosticMap();
      if (!(obj instanceof Map)) obj = Object.entries(obj);
      for (const [name, diagnostic] of obj) {
        if (!diagnostic) continue;
        diagnostics.set(name, diagnostic);
      }
      return diagnostics;
    }

    addDiagnostics(other) {
      for (const [name, otherDiagnostic] of other) {
        const myDiagnostic = this.get(name);

        if (myDiagnostic !== undefined &&
            myDiagnostic.canAddDiagnostic(otherDiagnostic)) {
          myDiagnostic.addDiagnostic(otherDiagnostic);
          continue;
        }

        // We need to avoid storing references to |otherDiagnostic| in both
        // |this| and |other| because future merge()s may add yet other
        // Diagnostics to |this|, and they shouldn't accidentally modify
        // anything in |other|.
        // Now, either |this| doesn't already have a Diagnostic named |name|
        // (myDiagnostic is undefined), or
        // |this| already has a Diagnostic named |name| that can't be merged
        // with |otherDiagnostic|.
        // Either way, we need to clone |otherDiagnostic|.
        // However, clones produced via fromDict/toDict cannot necessarily be
        // merged with yet other Diagnostics, either because of semantics (as in
        // the case of TelemtryInfo and the like) or because guids must not be
        // shared by distinct Diagnostics. Therefore, Diagnostics support
        // another way of cloning that is specifically targeted at supporting
        // merging: clone().

        const clone = otherDiagnostic.clone();

        if (myDiagnostic === undefined) {
          this.set(name, clone);
          continue;
        }

        // Now, |myDiagnostic| exists and it is unmergeable with |clone|, which
        // is safe to store in |this|.
        this.set(name, new tr.v.d.UnmergeableDiagnosticSet(
            [myDiagnostic, clone]));
      }
    }
  }

  return {DiagnosticMap};
});
</script>
